/*
 * Copyright (C) 2012 DataSymphony (http://datasymphony.com.au)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package au.com.ds.restty.js;

import java.util.Map;

import org.mozilla.javascript.ClassShutter;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;

import au.com.ds.restty.Config.ContextObjectMetadata;
import au.com.ds.restty.ExecutionContext;
import au.com.ds.restty.exc.JavaScriptError;
import au.com.ds.restty.js.co.ContextObject;

public class JavaScriptEngine {
	private static final String JS_RESULT_VAR = "$__execResultVar_";
	private static JavaScriptEngine cInstance = null;

	private static Context.ClassShutterSetter cShutterSetter = null;
	private static final ClassShutter cNoClassesShutter = new ClassShutter(){
		@Override
		public boolean visibleToScripts(String fullClassName) {
			return false;
		}				
	};

	private boolean mSingleton;
	private Context mContext;
	private Scriptable mScope;
	private boolean mIsCoPrepared = false;

	private JavaScriptEngine(boolean singleton){
		try {
			mSingleton = singleton;
			mContext = Context.enter();
			mScope = mContext.initStandardObjects();

			// do some magic to prevent java classes from being accessed for JS
			Context.ClassShutterSetter setter = mContext.getClassShutterSetter();
			if (setter == null) {
				setter = cShutterSetter;
			} else {
				cShutterSetter = setter;			
			}

			if (setter != null) {
				setter.setClassShutter(
						cNoClassesShutter
						);
			} else {
				try {
					mContext.setClassShutter(cNoClassesShutter);				
				} catch (Exception e){}
			}				
		} catch (Exception e){
			throw new JavaScriptError("Error during JavaScript engine initialization", e);
		}	
	}

	public static JavaScriptEngine getInstance(){
		if (cInstance == null) {
			cInstance = new JavaScriptEngine(true);
		}
		return cInstance;
	}

	public static JavaScriptEngine newInstance(){
		return new JavaScriptEngine(false);
	}

	public JsAnyObject execute(String jsCode, String fileName){
		mContext.evaluateString(mScope, "var " + JS_RESULT_VAR + " = " + jsCode + ";", fileName, 1, null);
		JsAnyObject result = get(JS_RESULT_VAR);
		mScope.put(JS_RESULT_VAR, mScope, Undefined.instance);
		return result;
	}

	public JsAnyObject get(String name){
		return JsObjectFactory.create(mScope.get(name, mScope));
	}

	public JsAnyObject call(JsFunction f){
		Object result = f.getValue().call(mContext, mScope, mScope, null);
		return JsObjectFactory.create(result);
	}

	public void close(){
		if (!mSingleton) {
			Context.exit();
		}
	}

	@SuppressWarnings("unchecked")
	public void prepareContextObjects() {
		if (mIsCoPrepared) {
			return;
		}		

		Map<String, ContextObjectMetadata> metadata = ExecutionContext.get().getConfig().getContextObjectsMetadata();

		for (Map.Entry<String, ContextObjectMetadata> item : metadata.entrySet()) {

			try {
				Class<? extends ContextObject> clazz = (Class<? extends ContextObject>) Class.forName(item.getValue().getClassName());
				ScriptableObject.defineClass(mScope, clazz);
				ContextObject co = (ContextObject) mContext.newObject(mScope, clazz.getSimpleName());
				JsObject settings = item.getValue().getSettings();
				if (settings != null) {
					co.setUp(settings);
				}
				mScope.put(item.getKey(), mScope, co);		
			} catch (Exception e) {
				throw new JavaScriptError("Erorr initializing Context Object: " + item.getKey(), e);
			}
		}
		mIsCoPrepared = true;
	}
}
